package com.nf.mvc.ioc;

import com.nf.mvc.MvcContext;
import com.nf.mvc.argument.BeanMethodArgumentResolver;
import com.nf.mvc.argument.MethodParameter;
import com.nf.mvc.handler.HandlerClass;
import com.nf.mvc.util.ReflectionUtils;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Stack;

public class BeanFactory {
    private BeanFactory() {
    }

    /**
     * 此方法用来实例化mvc框架管理的类型的实例,实例化这些类型时如果可能会自动注入配置属性类
     * <h3>使用地方</h3>
     * <p>整个mvc框架都是用这个方法来创建被mvc管理的类的对象，框架管理以下这些类型的实例化
     * <ul>
     *     <li>实例化扫描到的Mvc核心类，详见{@link com.nf.mvc.MvcContext#resolveMvcClass(Class, Class, List)},这些类型是单例的</li>
     *     <li>实例化控制器bean类型的方法参数，详见{@link BeanMethodArgumentResolver#resolveSetterArgument(MethodParameter, HttpServletRequest, Stack)},这些实例是原型的</li>
     *     <li>实例化用户编写的后端控制器，详见{@link HandlerClass#getHandlerObject()},这些实例是原型的</li>
     * </ul>
     * </p>
     * <h3>设计说明</h3>
     * <p>此方法本来是放在{@link ReflectionUtils}里的，原来的名字叫newInstance,由于添加的功能{@link #injectConfigurationProperties(Object)}与
     * 反射相关代码关联性不强，是一个跟mvc框架功能有关的功能，觉得放在{@link ReflectionUtils}不合适,
     * 所以，把这2个方法迁移过来，并把原来的newInstance方法改名为getBean</p>
     *
     * <p>此方法设计为静态是为了方便，也是因为我们不是一个容器框架，仅仅只是一个mvc框架，不需要实例化{@link BeanFactory}</p>
     *
     * <h3>重构技巧</h3>
     * <p>有很多地方使用了{@link ReflectionUtils#newInstance(Class)},怎么把他们都换成调用本类的getBean方法呢？重构步骤如下</p>
     * <ol>
     *     <li>选中newInstance方法，利用重构菜单中的Extract Delegate或者Move Members，
     *     把newInstance方法移动到新的类BeanFactory</li>
     *     <li>把迁移过来的newInstance方法重命名为getBean</li>
     *     <li>把原本的newInstance代码手动拷贝回ReflectionUtils里</li>
     *     <li>适当修改代码完成迁移</li>
     * </ol>
     *
     * @param clz 用来实例化的class
     * @return 注入了配置属性后的class的实例
     */
    public static <T> T getBean(Class<? extends T> clz) {
        T instance = ReflectionUtils.newInstance(clz);
        injectConfigurationProperties(instance);
        return instance;
    }

    /**
     * 此方法用来把配置属性类注入到某个被mvc框架的实例中去，只要这些实例有一个字段类型是某个配置属性类
     * 并且修饰了注解{@link Injected},比如下面的代码
     * <pre class="code">
     *  &#64;RequestMapping("/product")
     * public class ProductController {
     *
     *     &#64;Injected
     *     private MyConfigurationProperties1 config1;
     * }
     * </pre>
     *
     * @param instance 某个需要注入配置属性的实例
     * @param <T>      实例的类型
     */
    private static <T> void injectConfigurationProperties(T instance) {
        Field[] fields = instance.getClass().getDeclaredFields();

        for (Field field : fields) {
            if (field.isAnnotationPresent(Injected.class)) {
                Object configProperties = MvcContext.getMvcContext()
                        .getConfigurationProperties()
                        .get(field.getType());
                // 这就是所谓的自动注入实现的地方
                ReflectionUtils.setFieldValue(instance, field, configProperties);
            }
        }
    }
}
